/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.core.services;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.QueryResult;
import offset.nodes.Constants;
import offset.nodes.client.chooser.model.FindNodes;
import offset.nodes.client.chooser.model.FolderNode;
import offset.nodes.client.chooser.model.GetNodeByPath;
import offset.nodes.client.chooser.model.GetNodeByUuid;
import offset.nodes.client.chooser.model.GetPathFromUuid;
import offset.nodes.client.chooser.model.NewFolder;
import offset.nodes.client.chooser.model.NewNode;
import offset.nodes.client.chooser.model.ReorderNodes;
import offset.nodes.client.chooser.model.ReorderedFolder;
import offset.nodes.client.dialog.chooser.model.CopyNode;
import offset.nodes.client.dialog.chooser.model.MoveNode;
import offset.nodes.client.chooser.model.GetNodeTypeIcons;
import offset.nodes.client.chooser.model.GetPropertyTypeIcons;
import offset.nodes.client.dialog.newnode.model.NewDataFolder;
import offset.nodes.client.dialog.remove.model.HasReferences;
import offset.nodes.client.dialog.remove.model.RemoveNode;
import offset.nodes.client.editor.model.PrintHandler;
import offset.nodes.client.dialog.newnode.model.NewFile;
import offset.nodes.client.model.ServerRequest;
import offset.nodes.client.model.ServerResponse;
import offset.nodes.client.virtual.model.jcr.NodeWriter;
import offset.nodes.server.error.model.ExceptionId;
import offset.nodes.server.error.model.LoggedException;
import offset.nodes.server.icon.model.IconModel;
import offset.nodes.server.model.RepositoryModel;
import offset.nodes.server.search.model.DistinctNodeIterator;
import offset.nodes.server.search.model.SearchModel;
import offset.nodes.server.servlet.AbstractRepositoryService;
import offset.nodes.server.servlet.Service;
import offset.nodes.server.servlet.ServiceContainer;
import offset.nodes.server.servlet.ServiceMapping;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

/**
 *
 * @author Walter L�tgenau
 */
public class NodeServices extends AbstractRepositoryService implements ServiceContainer {

    @Override
    public ServiceMapping[] getServiceMappings() {
        return new ServiceMapping[]{
                    new ServiceMapping(new HasReferences.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return hasReferences((HasReferences.Request) request);
                }
            }, this),
                    new ServiceMapping(new NewFile.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return newFile((NewFile.Request) request);
                }
            }, this),
                    new ServiceMapping(new NewDataFolder.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return newDataFolder((NewDataFolder.Request) request);
                }
            }, this),
                    new ServiceMapping(new NewFolder.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return newFolder((NewFolder.Request) request);
                }
            }, this),
                    new ServiceMapping(new NewNode.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return newNode((NewNode.Request) request);
                }
            }, this),
                    new ServiceMapping(new CopyNode.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return copyNode((CopyNode.Request) request);
                }
            }, this),
                    new ServiceMapping(new GetNodeByPath.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return getNodeByPath((GetNodeByPath.Request) request);
                }
            }, this),
                    new ServiceMapping(new GetNodeByUuid.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return getNodeByUuid((GetNodeByUuid.Request) request);
                }
            }, this),
                    new ServiceMapping(new GetPathFromUuid.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return getPathFromUuid((GetPathFromUuid.Request) request);
                }
            }, this),
                    new ServiceMapping(new FindNodes.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return findNodes((FindNodes.Request) request);
                }
            }, this),
                    new ServiceMapping(new MoveNode.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return moveNode((MoveNode.Request) request);
                }
            }, this),
                    new ServiceMapping(new ReorderNodes.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return reorderNodes((ReorderNodes.Request) request);
                }
            }, this),
                    new ServiceMapping(new GetNodeTypeIcons.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return getNodeTypeIcons((GetNodeTypeIcons.Request) request);
                }
            }, this),
                    new ServiceMapping(new GetPropertyTypeIcons.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return getPropertyTypeIcons((GetPropertyTypeIcons.Request) request);
                }
            }, this),
                    new ServiceMapping(new RemoveNode.Request().getId(), new Service() {

                public Serializable service(ServerRequest request) throws Exception {
                    return removeNode((RemoveNode.Request) request);
                }
            }, this)
                };

    }
    
    /**
     * Return the node type icons configured in the repository.
     * 
     * @param request the request for node type icons
     * @return a response with a list of node type icons
     * 
     * @throws RepositoryException
     * @throws IOException 
     */
    public GetNodeTypeIcons.Response getNodeTypeIcons(GetNodeTypeIcons.Request request) throws RepositoryException, IOException {
        GetNodeTypeIcons.Response response = new GetNodeTypeIcons.Response(ServerResponse.RESULT_SUCCESS);
        
        IconModel iconModel = new IconModel(getSession(request));
        response.setIcons(iconModel.getNodeTypeIcons());
        
        return response;
    }

    /**
     * Return the property type icons configured in the repository.
     * 
     * @param request the request for property type icons
     * @return a response with a list of property type icons
     * 
     * @throws RepositoryException
     * @throws IOException 
     */
    public GetPropertyTypeIcons.Response getPropertyTypeIcons(GetPropertyTypeIcons.Request request) throws RepositoryException, IOException {
        GetPropertyTypeIcons.Response response = new GetPropertyTypeIcons.Response(ServerResponse.RESULT_SUCCESS);
        
        IconModel iconModel = new IconModel(getSession(request));
        response.setIcons(iconModel.getPropertyTypeIcons());
        
        return response;
    }

    /**
     * Check, if the node specified by path is still being referenced, or if nodes descending from the node are
     * are still being referenced.
     *
     * @param request
     * @return information whether the node or some of its descendants are still being referenced.
     */
    public HasReferences.Response hasReferences(HasReferences.Request request) throws RepositoryException {
        HasReferences.Response response = new HasReferences.Response(ServerResponse.RESULT_SUCCESS);

        Node node = null;
        if (request.getPath() != null && request.getPath().length() > 0)
            node = getNode(getSession(request), request.getPath());
        else if (request.getUuid() != null)
            node = getSession(request).getNodeByUUID(request.getUuid());
        
        if (node == null)
            throw new LoggedException(ExceptionId.GENERAL_PATH_INVALID, new Object[]{request.getPath()});

        if (node.getReferences().getSize() > 0)
            response.setReferences(HasReferences.Response.ReferenceResult.hasReference);
        else if (hasReferencesInSubtree(node))
            response.setReferences(HasReferences.Response.ReferenceResult.descendantHasReference);
        else
            response.setReferences(HasReferences.Response.ReferenceResult.noReference);

        return response;
    }

    protected NewFile.Response newFile(NewFile.Request request) throws RepositoryException {
        NewFile.Response response = new NewFile.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);
        RepositoryModel model = new RepositoryModel(session);

        Node node = getNode(session.getRootNode(), request.getPath());
        node = model.storeBlob(node.getParent(), node.getName(), request.getContentType(), request.getContent());
        node.setProperty(Constants.PROP_CONTENT_TYPE, Constants.CONTENT_TYPE_BINARY);

        if (request.isCommit())
            session.save();

        response.setUuid(node.getUUID());
        
        return response;
    }

    protected NewDataFolder.Response newDataFolder(NewDataFolder.Request request) throws RepositoryException {
        NewDataFolder.Response response = new NewDataFolder.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);
        Node node = getNode(session.getRootNode(), request.getPath());
        node.addNode(request.getName(), uriToPrefix(session, request.getPrimaryNodeType()));

        session.save();

        return response;
    }

    protected NewFolder.Response newFolder(NewFolder.Request request) throws RepositoryException {
        NewFolder.Response response = new NewFolder.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);
        Node node = getNode(session.getRootNode(), request.getPath());
        node = node.addNode(request.getName(), Constants.JCR_NT_UNSTRUCTURED);
        if (request.getDefaultContentType() != null)
            node.setProperty(Constants.PROP_DEFAULT_CONTENT_TYPE, request.getDefaultContentType());

        session.save();

        return response;
    }

    /**
     * Create a new, empty node. If a type is
     * given, it shall have the requested type.
     * 
     * @param request
     * @return
     * @throws RepositoryException
     */
    protected NewNode.Response newNode(NewNode.Request request) throws RepositoryException {
        NewNode.Response response = new NewNode.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);
        RepositoryModel model = new RepositoryModel(session);

        Node node = model.newNode(request.getPath(), request.getName(), request.getNodeType());

        if (node.isNodeType(Constants.JCR_REFERENCEABLE))
            response.setUuid(node.getUUID());

        return response;
    }

    public CopyNode.Response copyNode(CopyNode.Request request) throws RepositoryException, PathNotFoundException, SAXException {
        if (request.getToPath().startsWith(request.getFromPath()))
            throw new LoggedException(ExceptionId.NODE_COPY_RECURSIVE);

        CopyNode.Response response = new CopyNode.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);
        Node from = getNode(session.getRootNode(), request.getFromPath());

        boolean moveNode = false;
        String originalPath = request.getToPath();
        if (!session.getRootNode().hasNode(request.getToPath().substring(1))) {
            Node to = getNode(session.getRootNode(), request.getToPath());
            if (to.getParent().isSame(from.getParent())) {
                request.setToPath(to.getPath() + "_tmp");
                session.move(to.getPath(), request.getToPath());
                moveNode = true;
            }
        }

        ContentHandler importHandler = session.getImportContentHandler(request.getToPath(), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
        session.exportSystemView(request.getFromPath(), importHandler, false, false);

        if (moveNode) {
            Node to = getNode(session.getRootNode(), request.getToPath());
            session.move(request.getToPath() + Constants.PATH_SEPARATOR + from.getName(), originalPath);
            to.remove();
            response.setResultNodePath(originalPath);
        } else
            response.setResultNodePath(originalPath + Constants.PATH_SEPARATOR + from.getName());

        session.save();

        return response;
    }

    /**
     * Used to temporarily hold information about the order of a single folder
     */
    class OrderedFolder {
        Node parent;
        List<Node> children = new ArrayList<Node>();

        public OrderedFolder(Node parent) {
            this.parent = parent;
        }

        public List<Node> getChildren() {
            return children;
        }

        public void addChild(Node child) {
            this.children.add(child);
        }

        public Node getParent() {
            return parent;
        }

        public void setParent(Node parent) {
            this.parent = parent;
        }
    }

    /**
     * Used to test for a node sibling
     */
    class ChildNodeSet {
        protected HashSet<String> children = new HashSet<String>();

        public void add(Node node) throws RepositoryException {
            children.add(createKey(node));
        }

        public boolean contains(Node node) throws RepositoryException {
            return children.contains(createKey(node));
        }

        protected String createKey(Node node) throws RepositoryException {
            return node.getName() + ":" + node.getIndex();
        }
    }

    /**
     * Reorder a list of folders. Each folder has a FolderNode referring to a parent and a list of FolderNode(s) referring to its
     * children in the new order.
     *
     * @param request
     * @return
     * @throws RepositoryException
     * @throws PathNotFoundException
     * @throws SAXException
     */
    public ReorderNodes.Response reorderNodes(ReorderNodes.Request request) throws RepositoryException, PathNotFoundException, SAXException {
        ReorderNodes.Response response = new ReorderNodes.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);

        // Retrieve the real nodes and store them in a list of OneFolder, similar to the OrderedNodes for the FolderNode representation
        List<OrderedFolder>  reorderedFolders = new LinkedList<OrderedFolder>();
        for (ReorderedFolder orderedNodes : request.getReorderedNodes()) {
            OrderedFolder folder = new OrderedFolder(findNode(session, orderedNodes.getParent()));
            for (FolderNode child : orderedNodes.getChildren())
                folder.addChild(findNode(session, child));

            reorderedFolders.add(folder);
        }

        // for all reordered folders
        for (OrderedFolder folder : reorderedFolders) {
            // compare all children.

            ChildNodeSet reordered = new ChildNodeSet();

            // 1. Iterator for the new order
            Iterator<Node> newOrder = folder.getChildren().iterator();

            List<Node> orderBefore = new LinkedList<Node>();
            NodeIterator children = folder.getParent().getNodes();
            while (children.hasNext())
                orderBefore.add(children.nextNode());

            /* 2. Iterator for the old order. NodeIterator has not been used, as
             * the existing order will be modified.
             */
            Iterator<Node> oldOrder = orderBefore.iterator();
            Node oldChild = oldOrder.next();
            while (newOrder.hasNext() && oldChild != null) {
                Node newChild = newOrder.next();

                /* skip nodes, that have been reordered before */
                while(reordered.contains(oldChild) && oldOrder.hasNext())
                    oldChild = oldOrder.next();
                if (reordered.contains(oldChild))
                    break;

                if (newChild.isSame(oldChild))
                    // increment both lists.
                    if (oldOrder.hasNext())
                        oldChild = oldOrder.next();
                    else
                        break;
                else {
                    // insert the new child before the old child. Do not get the next old child.
                    String newRelativePath = newChild.getPath().substring(folder.getParent().getPath().length() + 1);
                    String oldRelativePath = oldChild.getPath().substring(folder.getParent().getPath().length() +1);
                    folder.getParent().orderBefore(newRelativePath, oldRelativePath);
                    reordered.add(newChild);
                }
            }
        }

        session.save();

        return response;
    }

    protected Node findNode(Session session, FolderNode folderNode) throws RepositoryException {
        if (folderNode.getUuid() != null)
            return session.getNodeByUUID(folderNode.getUuid());
        return getNode(session.getRootNode(), folderNode.getPath());
    }

    public GetNodeByUuid.Response getNodeByUuid(GetNodeByUuid.Request request) throws Exception {
        GetNodeByUuid.Response response = new GetNodeByUuid.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);
        Node node = session.getNodeByUUID(request.getUuid());

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        NodeWriter writer = new NodeWriter(node, new PrintHandler(out));
        writer.write();

        response.setNode(new String(out.toByteArray()));
        return response;
    }

    public GetPathFromUuid.Response getPathFromUuid(GetPathFromUuid.Request request) throws Exception {
        GetPathFromUuid.Response response = new GetPathFromUuid.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);
        Node node = session.getNodeByUUID(request.getUuid());

        response.setPath(node.getPath());
        return response;
    }

    public GetNodeByPath.Response getNodeByPath(GetNodeByPath.Request request) throws Exception {
        GetNodeByPath.Response response = new GetNodeByPath.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);

        // check, if node exists
        Node root = session.getRootNode();
        String path = request.getPath();
        if (path.startsWith("/"))
            path = path.substring(1);
        if (!root.hasNode(path)) {
            response.setNode(null);
            return response;
        }

        Node node = getNode(root, request.getPath());

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        NodeWriter writer = new NodeWriter(node, new PrintHandler(out));
        writer.write();

        response.setNode(new String(out.toByteArray()));
        return response;
    }

    /**
     * Find the nodes of the query and return them as FolderNode objects.
     * If the is equal to FILTER_ANY, add the path nodes to the result as well.
     *
     * @param request
     * @return
     * @throws RepositoryException
     */
    public FindNodes.Response findNodes(FindNodes.Request request) throws RepositoryException {
        FindNodes.Response response = new FindNodes.Response();
        response.setResultNodes(new ArrayList<FolderNode>());

        NodeIterator it = null;
        if ((request.getFilter() == null || request.getFilter().equals(FindNodes.FILTER_ANY))
                && request.getRoot() != null && request.getRoot().startsWith(Constants.PATH_SEPARATOR)) {
            String relativePath = request.getRoot().substring(1);
            Node root = getSession(request).getRootNode();
            Node node = null;
            if (relativePath.length() > 0) {
                node = getAncestorInPath(root, relativePath);
                if (node == null)
                    return response;
            } else
                node = root;

            if (request.isIncludePathNodes())
                addPathNodes(response.getResultNodes(), node);
            else
                addNodes(response.getResultNodes(), node.getNodes());
        } else {
            SearchModel model = new SearchModel(getSession(request));
            QueryResult result = model.executeXPathQuery(request.getRoot(), request.getFilter());
            addNodes(response.getResultNodes(), new DistinctNodeIterator(result.getNodes()));
        }

        return response;
    }

    /**
     * Return the bottom most node in the path.
     * 
     * @param root
     * @param relativePath
     * @return
     * @throws RepositoryException
     */
    protected Node getAncestorInPath(Node root, String relativePath) throws RepositoryException {
        Node result = null;

        while(result == null) {
            if (root.hasNode(relativePath))
                result = root.getNode(relativePath);

            if ( result == null && relativePath.indexOf(Constants.PATH_SEPARATOR) > 0)
                relativePath = relativePath.substring(0, relativePath.lastIndexOf(Constants.PATH_SEPARATOR));
            else
                break;
        }

        return result;
    }

    /**
     * Move a node from one destination to another. If one of the parent nodes
     * is versionable, the new state will be checked in.
     *
     * @param request
     * @return
     * @throws RepositoryException
     */
    public MoveNode.Response moveNode(MoveNode.Request request) throws RepositoryException {
        MoveNode.Response response = new MoveNode.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);
        Node root = session.getRootNode();

        Node from = root.getNode(request.getFromPath().substring(1)).getParent();

        session.move(request.getFromPath(), request.getToPath());
        session.save();

        Node to = root.getNode(request.getToPath().substring(1)).getParent();

        if (from.isNodeType(Constants.JCR_VERSIONABLE)) {
            from.checkin();
            from.checkout();
        }

        if (to.isNodeType(Constants.JCR_VERSIONABLE)) {
            to.checkin();
            to.checkout();
        }

        return response;
    }

    /**
     * Remove the node specified by path.
     *
     * @param request
     * @return the result of the removal, including the new path valid instead of the removed path.
     */
    public RemoveNode.Response removeNode(RemoveNode.Request request) throws RepositoryException {
        RemoveNode.Response response = new RemoveNode.Response(ServerResponse.RESULT_SUCCESS);

        Session session = getSession(request);
        
        Node node = null;
        if (request.getPath() != null && request.getPath().length() > 0)
            node = getNode(session, request.getPath());
        else if (request.getUuid() != null)
            node = session.getNodeByUUID(request.getUuid());
        
        if (node == null)
            throw new LoggedException(ExceptionId.GENERAL_PATH_INVALID, new Object[]{request.getPath()});

        response.setNewPath(node.getParent().getPath());
        node.remove();
        session.save();

        return response;
    }

    /**
     * Add the nodes of the path leading to node
     * to the result, including the node itself
     *
     * @param result
     * @param node
     * @throws RepositoryException
     */
    protected void addPathNodes(Collection<FolderNode> result, Node node) throws RepositoryException {
        Stack<Node> path = new Stack<Node>();

        while (node.getDepth() > 0) {
            path.push(node);
            node = node.getParent();
        }
        path.push(node);

        while (path.size() > 0) {
            node = path.pop();

            if (node.getDepth() == 0) // root
                result.add(new FolderNode(node));
            addNodes(result, node.getNodes());
        }
    }

    /**
     * Add the nodes as FolderNode objects to the result.
     *
     * @param result
     * @param i
     * @throws RepositoryException
     */
    protected void addNodes(Collection<FolderNode> result, NodeIterator i) throws RepositoryException {
        while (i.hasNext()) {
            Node node = i.nextNode();

            FolderNode resultNode = new FolderNode(node);
            result.add(resultNode);
        }
    }

    /**
     * Return a node with a relative path to the root. Create path nodes, if necessory
     *
     * @param root
     * @param path
     * @return
     * @throws javax.jcr.RepositoryException
     */
    protected Node getNode(Node root, String path) throws RepositoryException {
        RepositoryModel repositoryModel = new RepositoryModel(root.getSession());
        return repositoryModel.getNode(root, path);
    }

    protected Node getNode(Session session, String path) throws RepositoryException {
        Node root = session.getRootNode();
        if (path.startsWith("/"))
            path = path.substring(1);

        if (!root.hasNode(path))
            return null;
        return root.getNode(path);
    }

    protected String uriToPrefix(Session session, String name) throws RepositoryException {
        if (name.indexOf("}") < 0)
            return name;

        SessionImpl s = (SessionImpl) session;
        NodeTypeManagerImpl ntmgr = s.getNodeTypeManager();
        NamespaceRegistry nsr = session.getWorkspace().getNamespaceRegistry();

        StringTokenizer st = new StringTokenizer(name, "{}");
        String uri = null;
        String localname = null;

        if (st.hasMoreTokens())
            uri = st.nextToken();
        else
            return name;

        if (st.hasMoreTokens())
            localname = st.nextToken();
        else
            return name;

        String prefix = nsr.getPrefix(uri);

        return prefix + ":" + localname;
    }

    /**
     * Is the node or one of its descendant nodes still being referenced?
     *
     * @param node
     * @return the result
     * @throws RepositoryException
     */
    protected boolean hasReferencesInSubtree(Node node) throws RepositoryException {
        if (node.getReferences().getSize() > 0)
            return true;

        NodeIterator ni = node.getNodes();
        while (ni.hasNext()) {
            if (hasReferencesInSubtree(ni.nextNode()))
                return true;
        }

        return false;
    }
}
